import os
import sys
import base64
import hashlib
import time
import logging
from datetime import datetime
from pathlib import Path
from tempfile import gettempdir
from flask import Flask, request, jsonify, make_response
from flask.views import MethodView
import win32clipboard
import win32con
from PIL import Image
import io
import ctypes
from ctypes import wintypes
import json
from pathlib import Path

# 初始化日志
log = logging.getLogger('clipboard-service')
log.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
log.addHandler(handler)


api_version = '1'

# 从config.json加载配置
def load_config():
    config_path = Path('.') / 'config.json'
    with open(config_path, 'r', encoding='utf-8') as config_file:
        return json.load(config_file)


# 应用配置
class Config:
    def __init__(self):
        # 加载配置
        config_data = load_config()

        self.port = int(config_data.get("port", 8086))
        self.authkey = config_data.get("authkey", "123456")
        self.authkey_expired_timeout = config_data.get("authkeyExpiredTimeout", 30)
        self.logLevel = config_data.get("logLevel", "warning").upper()
        self.temp_dir = Path(config_data.get("tempDir", "./temp")).resolve()
        self.reserve_history = config_data.get("reserveHistory", False)
        self.notify_copy = config_data.get("notify", {}).get("copy", False)
        self.notify_paste = config_data.get("notify", {}).get("paste", True)

        # 设置日志级别
        numeric_level = getattr(logging, self.logLevel, None)
        if not isinstance(numeric_level, int):
            raise ValueError(f'Invalid log level: {self.logLevel}')
        log.setLevel(numeric_level)

app = Flask(__name__)
app.app_config= Config()

# 确保临时目录存在
app.app_config.temp_dir.mkdir(parents=True, exist_ok=True)


# Windows剪贴板工具类
class Clipboard:
    @staticmethod
    def content_type():
        try:
            win32clipboard.OpenClipboard()
            if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_UNICODETEXT):
                return "text"
            elif win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB):
                return "bitmap"
            elif win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_HDROP):
                return "file"
            return "unknown"
        finally:
            win32clipboard.CloseClipboard()

    @staticmethod
    def text():
        try:
            win32clipboard.OpenClipboard()
            if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_UNICODETEXT):
                return win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
            return ""
        finally:
            win32clipboard.CloseClipboard()

    @staticmethod
    def set_text(text):
        try:
            win32clipboard.OpenClipboard()
            win32clipboard.EmptyClipboard()
            win32clipboard.SetClipboardText(text, win32clipboard.CF_UNICODETEXT)
            return True
        except Exception as e:
            log.error(f"设置文本到剪贴板失败: {str(e)}")
            return False
        finally:
            win32clipboard.CloseClipboard()

    @staticmethod
    def bitmap():
        try:
            win32clipboard.OpenClipboard()
            if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB):
                return win32clipboard.GetClipboardData(win32clipboard.CF_DIB)
            return None
        finally:
            win32clipboard.CloseClipboard()

    @staticmethod
    def files():
        try:
            win32clipboard.OpenClipboard()
            if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_HDROP):
                # 使用ctypes处理HDROP格式
                hdrop = win32clipboard.GetClipboardData(win32clipboard.CF_HDROP)
                file_count = ctypes.windll.shell32.DragQueryFileW(hdrop, -1, None, 0)
                files = []
                buf = ctypes.create_unicode_buffer(260)
                for i in range(file_count):
                    ctypes.windll.shell32.DragQueryFileW(hdrop, i, buf, ctypes.sizeof(buf))
                    files.append(buf.value)
                return files
            return []
        finally:
            win32clipboard.CloseClipboard()

    @staticmethod
    def set_files(file_paths):
        if not file_paths:
            return False

        # 将文件路径转换为宽字符字符串
        file_paths = [str(path) for path in file_paths]
        buffer = "\0".join(file_paths) + "\0\0"

        try:
            win32clipboard.OpenClipboard()
            win32clipboard.EmptyClipboard()

            # 准备HDROP结构
            hdrop_size = 4 + len(buffer.encode("utf-16le")) + 2
            hglobal = ctypes.windll.kernel32.GlobalAlloc(0x2000, hdrop_size)  # GHND
            hdrop_ptr = ctypes.windll.kernel32.GlobalLock(hglobal)

            # 写入DROPFILES结构
            dropfiles = DROPFILES()
            dropfiles.pFiles = 20  # DROPFILES结构的大小
            dropfiles.pt = (0, 0)
            dropfiles.fNC = 0
            dropfiles.fWide = 1  # Unicode

            ctypes.memmove(hdrop_ptr, ctypes.byref(dropfiles), ctypes.sizeof(dropfiles))

            # 写入文件路径
            data_ptr = ctypes.c_void_p(hdrop_ptr + ctypes.sizeof(dropfiles))
            ctypes.cdll.msvcrt.wcscpy(ctypes.c_wchar_p(data_ptr), buffer)

            ctypes.windll.kernel32.GlobalUnlock(hglobal)
            win32clipboard.SetClipboardData(win32clipboard.CF_HDROP, hglobal)
            return True
        except Exception as e:
            log.error(f"设置文件到剪贴板失败: {str(e)}")
            return False
        finally:
            win32clipboard.CloseClipboard()


# DROPFILES结构定义
class DROPFILES(ctypes.Structure):
    _fields_ = [
        ("pFiles", wintypes.DWORD),
        ("pt", wintypes.POINT),
        ("fNC", wintypes.BOOL),
        ("fWide", wintypes.BOOL)
    ]


# 中间件装饰器
def client_name(f):
    def decorated(*args, **kwargs):
        client_name = request.headers.get("X-Client-Name", "匿名设备")
        request.client_name = client_name
        return f(*args, **kwargs)

    return decorated

# 检查版本
def api_version_checker(f):
    def decorated(*args, **kwargs):

        get_version=request.headers.get("X-API-Version")
        if get_version != api_version:
            return jsonify({"error": "接口版本不匹配，请升级您的捷径"}), 400
        return f(*args, **kwargs)

    return decorated

#验证服务
def auth(f):
    def decorated(*args, **kwargs):
        if not app.app_config.authkey:
            return f(*args, **kwargs)

        req_auth = request.headers.get("X-Auth")
        if not req_auth:
            return jsonify({"error": "缺少认证头"}), 403
        timestamp = int(time.time())
        time_key = timestamp // app.app_config.authkey_expired_timeout
        auth_code_raw = f"{app.app_config.authkey}.{time_key}"
        auth_code_hash = hashlib.md5(auth_code_raw.encode()).hexdigest()
        # print(req_auth, auth_code_hash)
        if auth_code_hash != req_auth:
            return jsonify({"error": "操作被拒绝：Authkey 验证失败"}), 403

        return f(*args, **kwargs)
    return decorated

#日志记录
def log_request(f):
    def decorated(*args, **kwargs):
        start_time = time.time()
        response = f(*args, **kwargs)
        duration = time.time() - start_time

        log_data = {
            "method": request.method,
            "path": request.path,
            "status": response[1] if isinstance(response, tuple) else response.status_code,
            "duration": f"{duration:.4f}s",
            "client_ip": request.remote_addr,
            "client_name": getattr(request, "client_name", "未知设备")
        }

        log.info(f"{log_data['method']} {log_data['path']} - {log_data['status']} | "
                 f"Client: {log_data['client_name']} ({log_data['client_ip']}) | "
                 f"Time: {log_data['duration']}")

        return response

    return decorated


# 剪贴板视图
class ClipboardView(MethodView):
    decorators = [client_name, api_version_checker, auth, log_request]

    def get(self):
        content_type = Clipboard.content_type()

        if content_type == "text":
            text = Clipboard.text()
            log.info("获取剪贴板文本")
            self._send_copy_notification(text)
            return jsonify({"type": "text", "data": text})

        elif content_type == "bitmap":
            bmp_bytes = Clipboard.bitmap()
            if not bmp_bytes:
                return jsonify({"error": "无法获取位图数据"}), 400

            # 转换BMP为PNG
            try:
                img = Image.open(io.BytesIO(bmp_bytes))
                png_buffer = io.BytesIO()
                img.save(png_buffer, format="PNG")
                png_data = base64.b64encode(png_buffer.getvalue()).decode()

                log.info("获取剪贴板图片")
                self._send_copy_notification("[图片媒体] 被复制")
                return jsonify({
                    "type": "file",
                    "data": [{"name": "clipboard.png", "content": png_data}]
                })
            except Exception as e:
                log.error(f"图片转换失败: {str(e)}")
                return jsonify({"error": "图片处理失败"}), 500

        elif content_type == "file":
            files = Clipboard.files()
            if not files:
                return jsonify({"error": "无法获取文件列表"}), 400

            response_files = []
            for file_path in files:
                try:
                    with open(file_path, "rb") as f:
                        content = base64.b64encode(f.read()).decode()
                    response_files.append({
                        "name": Path(file_path).name,
                        "content": content
                    })
                except Exception as e:
                    log.error(f"读取文件失败: {file_path} - {str(e)}")

            log.info(f"获取剪贴板文件: {len(response_files)}个文件")
            self._send_copy_notification("[文件] 被复制")
            return jsonify({"type": "file", "data": response_files})

        return jsonify({"error": "无法识别剪切板内容"}), 400

    def post(self):
        if not app.app_config.reserve_history:
            self._clean_temp_files()

        content_type = request.headers.get("X-Content-Type")

        if content_type == "text":
            return self._set_text()
        else:
            return self._set_file(content_type)

    def _set_text(self):
        data = request.json
        if not data or "data" not in data:
            return jsonify({"error": "无效的请求体"}), 400

        text = data["data"]
        if Clipboard.set_text(text):
            notify_text = text if text else "粘贴内容为空"
            self._send_paste_notification(notify_text)
            log.info(f"设置剪贴板文本: {text[:50]}...")
            return "", 204
        return jsonify({"error": "设置剪贴板失败"}), 500

    def _set_file(self, content_type):
        data = request.json
        if not data or "data" not in data:
            return jsonify({"error": "无效的请求体"}), 400

        files = data["data"]
        if not isinstance(files, list):
            return jsonify({"error": "无效的文件数据"}), 400

        saved_paths = []
        for file_data in files:
            name = file_data.get("name")
            content = file_data.get("content")
            if not name or not content:
                continue

            try:
                file_bytes = base64.b64decode(content)
                file_path = self._get_unique_filename(name)
                with open(file_path, "wb") as f:
                    f.write(file_bytes)
                saved_paths.append(file_path)
            except Exception as e:
                log.error(f"保存文件失败: {name} - {str(e)}")

        # 记录文件路径用于清理
        self._set_last_filenames(saved_paths)

        if Clipboard.set_files(saved_paths):
            notify_text = "[图片媒体] 已复制到剪贴板" if content_type == "media" else "[文件] 已复制到剪贴板"
            self._send_paste_notification(notify_text)
            log.info(f"设置剪贴板文件: {len(saved_paths)}个文件")
            return "", 204
        return jsonify({"error": "设置剪贴板失败"}), 500

    def _get_unique_filename(self, filename):
        counter = 0
        base_name, ext = os.path.splitext(filename)
        while True:
            new_name = f"{base_name}{f'_{counter}' if counter else ''}{ext}"
            path = app.app_config.temp_dir / new_name
            if not path.exists():
                return path
            counter += 1

    def _set_last_filenames(self, filenames):
        path = app.app_config.temp_dir / "_filename.txt"
        with open(path, "w", encoding="utf-8") as f:
            f.write("\n".join(str(p) for p in filenames))

    def _clean_temp_files(self):
        path = app.app_config.temp_dir / "_filename.txt"
        if path.exists():
            try:
                with open(path, "r", encoding="utf-8") as f:
                    for file_path in f:
                        file_path = file_path.strip()
                        if file_path and os.path.exists(file_path):
                            os.remove(file_path)
                os.remove(path)
            except Exception as e:
                log.error(f"清理临时文件失败: {str(e)}")

    def _send_copy_notification(self, text):
        if app.app_config.notify_copy:
            self._send_notification("复制", text)

    def _send_paste_notification(self, text):
        if app.app_config.notify_paste:
            self._send_notification("粘贴", text)

    def _send_notification(self, action, text):
        client = getattr(request, "client_name", "未知设备")
        title = f"{action}自 {client}"
        # 实际应用中应使用桌面通知库，这里仅模拟
        log.info(f"发送通知: {title} - {text}")


# 注册路由
app.add_url_rule('/', view_func=ClipboardView.as_view('clipboard'))

# 启动应用
if __name__ == '__main__':
    log.info(f"服务启动，临时目录: {app.app_config.temp_dir}")
    log.info(f"认证密钥: {'已设置' if app.app_config.authkey else '未设置'}")
    app.run(host='0.0.0.0', port=app.app_config.port)